package model;

import util.Pair;
import util.Util;

import java.util.*;

/**
 * @author Alex Vikharev alex.vikharev@gmail.com
 *         created 25.01.12
 */
public final class Field {

    private static final Comparator<Field> COMPARATOR = new Comparator<Field>() {
        @Override
        public int compare(Field o1, Field o2) {
            for (int y = 0; y < 4; y++) {
                for (int x = 0; x < 4; x++) {
                    int code1 = o1.get(x, y) == null ? -1 : o1.get(x, y).code;
                    int code2 = o2.get(x, y) == null ? -1 : o2.get(x, y).code;
                    if (code1 != code2) {
                        return code1 - code2;
                    }
                }
            }
            return 0;
        }
    };

    private final Figure[][] figures;
    private final Set<Figure> usedFigures;

    private Field(Figure[][] figures, EnumSet<Figure> usedFigures) {
        this.figures = figures;
        this.usedFigures = Collections.unmodifiableSet(usedFigures);
    }

    public Figure get(int x, int y) {
        return figures[y][x];
    }

    public Field addFigure(int x, int y, Figure figure) {
        return builder().copyField(this).setFigure(x, y, figure).build();
    }

    public Field removeFigure(int x, int y) {
        return builder().copyField(this).removeFigure(x, y).build();
    }

    public List<Field> getAllSymmetric() {
        List<Field> result = new ArrayList<>();

        Field fp = flip();
        Field f = this;

        result.add(f);
        result.add(fp);

        for (int i = 0; i < 3; i++) {
            f = f.rotate();
            fp = fp.rotate();

            result.add(f);
            result.add(fp);
        }
        return result;
    }

    public Field rotate() {
        FieldBuilder builder = builder();
        for (int x = 0; x < 4; x++) {
            for (int y = 0; y < 4; y++) {
                if (figures[y][x] != null) {
                    builder.setFigure(y, 3 - x, figures[y][x]);
                }
            }
        }
        return builder.build();
    }

    public Field flip() {
        FieldBuilder builder = builder();
        for (int x = 0; x < 4; x++) {
            for (int y = 0; y < 4; y++) {
                if (figures[y][x] != null) {
                    builder.setFigure(x, 3 - y, figures[y][x]);
                }
            }
        }
        return builder.build();
    }

    public Field permuteBitsInFigures(int[] permutation) {
        FieldBuilder builder = builder();
        for (int x = 0; x < 4; x++) {
            for (int y = 0; y < 4; y++) {
                if (figures[y][x] != null) {
                    builder.setFigure(x, y, figures[y][x].permute(permutation));
                }
            }
        }
        return builder.build();
    }

    public Field invertBitsInFigures() {
        FieldBuilder builder = builder();
        for (int x = 0; x < 4; x++) {
            for (int y = 0; y < 4; y++) {
                if (figures[y][x] != null) {
                    builder.setFigure(x, y, figures[y][x].invert());
                }
            }
        }
        return builder.build();
    }

    public Field xorBitsInFigures(int xor) {
        FieldBuilder builder = builder();
        for (int x = 0; x < 4; x++) {
            for (int y = 0; y < 4; y++) {
                if (figures[y][x] != null) {
                    builder.setFigure(x, y, figures[y][x].xor(xor));
                }
            }
        }
        return builder.build();
    }

    public static Pair<Integer, Integer> getFirstDiffPos(Field field1, Field field2) {
        for (int y = 0; y < 4; y++) {
            for (int x = 0; x < 4; x++) {
                if (field1.get(x, y) != field2.get(x, y)) {
                    return Pair.of(x, y);
                }
            }
        }
        return null;
    }

    /**
     * Returns an unmodifiable set contains used figures.
     *
     * @return unmodifiable set of figures on board.
     */

    public Set<Figure> getUsedFigures() {
        return usedFigures;
    }

    public boolean isGameOver(GameType type) {
        return type.equals(GameType.SIMPLE) ? isSimpleGameOver() : isComplexGameOver();
    }

    public boolean isSimpleGameOver() {
        return isRowComplete(0) || isRowComplete(1) || isRowComplete(2) || isRowComplete(3) ||
                isColumnComplete(0) || isColumnComplete(1) || isColumnComplete(2) || isColumnComplete(3) ||
                isFirstDiagComplete() || isSecondDiagComplete();
    }

    public boolean isComplexGameOver() {
        return isSimpleGameOver() || isAnySquareComplete();
    }

    private boolean isRowComplete(int row) {
        return Util.hasCommonPart(figures[row][0], figures[row][1], figures[row][2], figures[row][3]);
    }

    private boolean isColumnComplete(int column) {
        return Util.hasCommonPart(figures[0][column], figures[1][column], figures[2][column], figures[3][column]);
    }

    private boolean isFirstDiagComplete() {
        return Util.hasCommonPart(figures[0][0], figures[1][1], figures[2][2], figures[3][3]);
    }

    private boolean isSecondDiagComplete() {
        return Util.hasCommonPart(figures[0][3], figures[1][2], figures[2][1], figures[3][0]);
    }

    private boolean isSquareComplete(int x, int y) {
        return Util.hasCommonPart(figures[y][x], figures[y][x + 1], figures[y + 1][x], figures[y + 1][x + 1]);
    }

    private boolean isAnySquareComplete() {
        return isSquareComplete(0, 0) || isSquareComplete(0, 1) || isSquareComplete(0, 2) ||
                isSquareComplete(1, 0) || isSquareComplete(1, 1) || isSquareComplete(1, 2) ||
                isSquareComplete(2, 0) || isSquareComplete(2, 1) || isSquareComplete(2, 2);
    }

    public static FieldBuilder builder() {
        return new FieldBuilder();
    }

    public static class FieldBuilder {
        private final Figure[][] figures = new Figure[4][4];
        private final EnumSet<Figure> usedFigures = EnumSet.noneOf(Figure.class);

        public FieldBuilder setFigure(int x, int y, Figure figure) {
            if (x < 0 || x > 3 || y < 0 || y > 3) {
                throw new IllegalArgumentException("PositionType arguments must be in range from 0 to 3 inclusive");
            }
            if (!usedFigures.add(figure)) {
                throw new RuntimeException("Attempt to set one figure twice");
            }
            if (figure == null) {
                throw new NullPointerException("Figure must not be null");
            }
            if (figures[y][x] != null) {
                throw new IllegalStateException("Attempt to replace figure");
            }
            figures[y][x] = figure;
            return this;
        }

        public FieldBuilder copyField(Field field) {
            for (int i = 0; i < field.figures.length; i++) {
                Figure[] line = field.figures[i];
                for (int j = 0; j < line.length; j++) {
                    Figure figure = line[j];
                    if (figure != null) {
                        setFigure(j, i, figure);
                    }
                }
            }
            return this;
        }

        public Field build() {
            return new Field(figures, usedFigures);
        }

        public FieldBuilder removeFigure(int x, int y) {
            if (x < 0 || x > 3 || y < 0 || y > 3) {
                throw new IllegalArgumentException("PositionType arguments must be in range from 0 to 3 inclusive");
            }
            if (figures[y][x] != null) {
                usedFigures.remove(figures[y][x]);
                figures[y][x] = null;
            }
            return this;
        }

    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (int y = 3; y >= 0; y--) {
            for (int x = 0; x < 4; x++) {
                int code = get(x, y) == null ? -1 : get(x, y).code;
                if (code == -1) {
                    sb.append("*    ");
                } else {
                    sb.append(toBinary(code)).append(" ");
                }
            }
            sb.append("\n");
        }
        return sb.toString();
    }


    private String toBinary(int code) {
        return String.format("%4s", Integer.toBinaryString(code)).replace(' ', '0');
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Field field = (Field) o;

        return Arrays.deepEquals(field.figures, figures);
    }

    @Override
    public int hashCode() {
        int result = usedFigures != null ? usedFigures.hashCode() : 0;
        result = 31 * result + Arrays.hashCode(figures[0]);
        result = 31 * result + Arrays.hashCode(figures[1]);
        result = 31 * result + Arrays.hashCode(figures[2]);
        result = 31 * result + Arrays.hashCode(figures[3]);
        return result;
    }

    public static Comparator<Field> comparator() {
        return COMPARATOR;
    }
}
